1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.util..string;
12 public import hip.util.conv:to;
13 public import hip.util.to_string_range;
14 import core.stdc.string;
15 
16 version(WebAssembly) version = UseDRuntimeDecoder;
17 version(CustomRuntimeTest) version = UseDRuntimeDecoder;
18 version(PSVita) version = UseDRuntimeDecoder;
19 version(WebAssembly) version = AvoidStringFragmentation;
20 
21 /** 
22  *  RefCounted, @nogc string, OutputRange compatible, 
23  */
24 struct String
25 {
26     @nogc:
27     import core.stdc.stdlib;
28     import core.int128;
29     char[] chars;
30     private size_t _capacity;
31     private int* countPtr;
32     size_t length() @safe pure const nothrow  {return chars.length;}
33 
34     this(this) nothrow @safe pure
35     {
36         if(countPtr !is null)
37             *countPtr = *countPtr + 1;
38     }
39 
40     char* ptr(){ return chars.ptr;}
41 
42     private void initialize(size_t length)
43     {
44         if(length == 0)
45             length = 128;
46         this.countPtr = cast(int*)malloc(int.sizeof);
47         this.chars = (cast(char*)malloc(length))[0..0];
48         this._capacity = length;
49         this.chars.ptr[0.._capacity] = '\0';
50         *countPtr = 1;
51     }
52 
53     static auto opCall(string str)
54     {
55         String s;
56         s.initialize(str.length);
57         s.chars = s.chars.ptr[0..str.length];
58         s.chars[] = str[];
59         return s;
60     }
61     static auto opCall(const(char)* str){return opCall(str[0..strlen(str)]);}
62     static auto opCall(String str){return str;}
63 
64     private enum isAppendable(T) = is(T == string) || is(T == immutable(char)*) || is(T == char);
65 
66     version(AvoidStringFragmentation)
67     {
68         static auto opCall(Args...)(Args args)
69         {
70             import hip.util.conv:toStringRange;
71             rcStringBuffer.clear();
72             static foreach(a; args)
73             {
74                 static if(isAppendable!(typeof(a)) )
75                     rcStringBuffer~= a;
76                 else static if(__traits(hasMember, a, "toString"))
77                     rcStringBuffer~= a.toString;
78                 else static if(is(typeof(a) == struct) || __traits(compiles, toStringRange(rcStringBuffer, a)))
79                 {
80                     toStringRange(rcStringBuffer, a);
81                 }
82                 // else static if(is(typeof(a) == String[]))
83                 // {
84                 //     foreach(str; a)
85                 //         s~= str;
86                 // }
87                 else
88                     static assert(false, "No conversion found for type "~typeof(a).stringof);
89             }
90             static foreach_reverse(a; args)
91             {
92                 static if(is(typeof(a) == String))
93                     destroy(a);
94             }
95             return String(rcStringBuffer.toString());
96         }
97         auto ref opOpAssign(string op, T)(T value)
98         if(op == "~")
99         {
100             rcStringBuffer.clear();
101             char[] chs;
102             static if(is(T == String))
103                 chs = value.chars;
104             else static if (is(T == string) || is(T == char[]))
105                 chs = cast(char[])value;
106             else static if(is(T == immutable(char)*))
107                 chs = value[0..strlen(value)];
108             else static if(is(T == char))
109             {
110                 char[1] _chContainer;
111                 _chContainer[0] = value;
112                 chs = _chContainer;
113             }
114             else
115             {
116                 toStringRange(rcStringBuffer, value);
117                 chs = cast(char[])rcStringBuffer.toString();
118             }
119             if(!updateBorrowed(chs.length) && chs.length + this.length >= this._capacity) //New size is greater than capacity
120                 resize(cast(uint)((chs.length + this.length)*1.5));
121             memcpy(chars.ptr+length, chs.ptr, chs.length);
122             chars = chars.ptr[0..chars.length+chs.length];
123             return this;
124         }
125     }
126     else
127     {
128         static auto opCall(Args...)(Args args)
129         {
130             import hip.util.conv:toStringRange;
131             String s;
132             s.initialize(128);
133             static foreach(a; args)
134             {
135                 static if(isAppendable!(typeof(a)) )
136                     s~= a;
137                 else static if(__traits(hasMember, a, "toString"))
138                     s~= a.toString;
139                 else static if(is(typeof(a) == struct) || __traits(compiles, toStringRange(s, a)))
140                 {
141                     toStringRange(s, a);
142                 }
143                 // else static if(is(typeof(a) == String[]))
144                 // {
145                 //     foreach(str; a)
146                 //         s~= str;
147                 // }
148                 else static assert(false, "No conversion found for type "~typeof(a).stringof);
149             }
150             return s;
151         }
152         auto ref opOpAssign(string op, T)(T value)
153         if(op == "~")
154         {
155             String temp;
156             char[] chs;
157             static if(is(T == String))
158                 chs = value.chars;
159             else static if (is(T == string) || is(T == char[]))
160                 chs = cast(char[])value;
161             else static if(is(T == immutable(char)*))
162                 chs = value[0..strlen(value)];
163             else static if(is(T == char))
164             {
165                 char[1] _chContainer;
166                 _chContainer[0] = value;
167                 chs = _chContainer;
168             }
169             else
170             {
171                 temp = String(value);
172                 chs = temp.chars;
173             }
174             if(!updateBorrowed(chs.length) && chs.length + this.length >= this._capacity) //New size is greater than capacity
175                 resize(cast(uint)((chs.length + this.length)*1.5));
176             memcpy(chars.ptr+length, chs.ptr, chs.length);
177             chars = chars.ptr[0..chars.length+chs.length];
178             return this;
179         }
180     }
181 
182 
183     alias _opApplyFn = int delegate(char c) @nogc;
184     int opApply(scope _opApplyFn dg)
185     {
186         int result = 0;
187         for(int i = 0; i < length && result; i++)
188             result = dg(chars[i]);
189         return result;
190     }
191 
192     /**
193     *   If it was borrowed, allocate new memory.
194     */
195     bool updateBorrowed(size_t length)
196     {
197         if(countPtr == null) //Not initialized
198         {  
199             initialize(length);
200             return true;
201         }
202         else if(*countPtr != 1) //If it is borrowed
203         {
204             //Remove that old reference and initialize itself (something like when slices shares a common array)
205             char[] oldChars = chars;
206             *countPtr = *countPtr - 1;
207             initialize(length+this.length);
208             chars = chars.ptr[0..oldChars.length];
209             chars[0..oldChars.length] = oldChars[0..$];
210             return true;
211         }
212         return false;
213     }
214 
215 
216     auto ref opAssign(string value)
217     {
218         if(countPtr is null)
219             chars = cast(char[])value; //Don't allocate memory for the string literal.
220         else
221         {
222             bool resized = updateBorrowed(value.length);
223             if(!resized)
224             {
225                 if(chars == null)
226                     initialize(value.length);
227                 else if(value.length > _capacity)
228                     resize(value.length);
229             }
230             chars.ptr[0..value.length] = value[];
231         }
232         return this;
233     }
234 
235     auto ref opAssign(immutable(char)* value)
236     {
237         opAssign(value[0..strlen(value)]);
238         return this;
239     }
240 
241 
242     bool opCast(T: bool)() const nothrow { return chars.ptr != null; }
243 
244     string opCast(T: string)() const
245     {
246         return cast(string)chars[0..length];
247     }
248 
249     String opSlice(size_t start, size_t end) nothrow
250     {
251         assert(countPtr != null, "Can't slice a null pointer.");
252         String ret;
253         ret.countPtr = countPtr;
254         ret.chars = chars.ptr[start..end];
255         ret._capacity = _capacity;
256         *countPtr = *countPtr+1;
257         return ret;
258     }
259 
260     /**
261      * BEWARE: If you're creating a String from a String.toString call, that String will
262      * cause memory fragmentation, which will make WebAssembly not reuse that memory block.
263      ```d
264      String s = String(String("Hello").toString);
265      ```
266      * Since s won't recognize that "Hello" as a
267      *
268      * Returns:
269      */
270     string toString() const pure nothrow @trusted
271     {
272         return cast(string)chars;
273     }
274 
275     pragma(inline, true) private void resize(size_t newSize)
276     {
277         chars = (cast(char*)realloc(chars.ptr, newSize))[0..chars.length];
278         _capacity = newSize;
279     }
280     ///Make this struct OutputRange compatible
281     void put(char c)
282     {
283         if(this.length + 1 >= this._capacity)
284             resize(cast(uint)((this.length+1)*1.5));
285         chars.ptr[length] = c;
286         chars = chars.ptr[0..length+1];
287     }
288     bool opEquals(R)(const R other) const
289     {
290         static if(is(R == typeof(null)))
291             return chars == null;
292         else static if(is(R == string))
293             return toString == other;
294         else static if(is(R == String))
295             return toString == other.toString;
296         else static assert(false, "Invalid comparison between String and "~R.stringof);
297     }
298     
299     /**
300     *   This function serves to allocate before put. This will make less allocations occur while iterating
301     * this struct as an OutputRange.
302     */
303     void preAllocate(uint howMuch)
304     {
305         if(length + howMuch > _capacity)
306             resize(_capacity + howMuch);
307     }
308     void preAllocate(ulong howMuch){preAllocate(cast(uint)howMuch);}
309 
310     ref auto opIndex(size_t index) const
311     {
312         assert(index < length, "Index out of bounds");
313         return chars[index];
314     }
315 
316     ~this() nothrow @trusted pure
317     {
318         import core.memory;
319         if(countPtr != null)
320         {
321             *countPtr = *countPtr - 1;
322             assert(*countPtr >= 0);
323             if(*countPtr == 0)
324             {
325                 pureFree(chars.ptr);
326                 pureFree(countPtr);
327             }
328             countPtr = null;
329             chars = null;
330         }
331     }
332 
333 }
334 
335 /**
336  * Creates a stack string. This does not use any heap allocation.
337  * Prefer using that one inside game loop
338  */
339 struct StringBuffer(size_t capacity)
340 {
341     @nogc:
342     private char[capacity] chars;
343     private size_t _length;
344 
345     pragma(inline, true)
346     size_t length() @safe pure const nothrow { return _length; }
347 
348     /**
349      * Returns: An empty StringBuffer which avoids initialization on its buffer
350      */
351     static StringBuffer!(capacity) get()
352     {
353         StringBuffer!(capacity) ret = void;
354         ret._length = 0;
355         return ret;
356     }
357 
358     static auto opCall(Args...)(Args args)
359     {
360         auto ret = StringBuffer!(capacity).get();
361         static foreach(a; args)
362             ret~= a;
363         return ret;
364     }
365 
366     void preAllocate(size_t howMuch)
367     {
368         assert(length + howMuch < capacity, "Can't preallocate more to string buffer.");
369     }
370     void put(char c){chars[_length++] = c;}
371     void put(const(char)[] s){chars[_length.._length+s.length] = s[]; _length+= s.length;}
372     void put(immutable(char)* s){put(s[0..strlen(s)]);}
373     void put(String s){put(s.toString());}
374 
375     StringBuffer opSlice(size_t start, size_t end)
376     {
377         assert(end >= start, "Slice end must be greater or equal than start.");
378         StringBuffer ret = void;
379         ret.chars[0..end-start] = chars[start..end];
380         ret._length = end - start;
381         return ret;
382     }
383 
384     pragma(inline, true)
385     ref char opIndex(size_t index)
386     {
387         return chars[index];
388     }
389 
390     void opOpAssign(string op, T)(T value)
391     if(op == "~")
392     {
393         static if(is(T == char) || is(T : const(char)[]) || is(T == immutable(char*)) || is(T == String))
394             put(value);
395         else
396             toStringRange(this, value);
397     }
398     void clear(){_length = 0;}
399     bool opCast(T : bool)() const{return _length != 0;}
400     bool opEquals(const string other) const{return chars[0.._length] == other;}
401     string toString() const @trusted {return cast(string)chars[0.._length];}
402 }
403 
404 alias BigString = StringBuffer!(8192);
405 alias PathString = StringBuffer!(2048);
406 alias SmallString = StringBuffer!(256);
407 
408 
409 version(AvoidStringFragmentation)
410 {
411     ///On WebAssembly, it is required to have a refcounted string buffer for avoiding memory fragmentation, which causes refcounted strings to leak memory when appended
412     private StringBuffer!(8192) rcStringBuffer;
413 }
414 
415 struct StringBuilder
416 {
417     private char[] builtString;
418     private uint builtLength;
419     string[] strings;
420     private uint stringsPtr = 0;
421     
422     void append(T)(T value)
423     {
424         if(stringsPtr == strings.length)
425         {
426             if(strings.length == 0x10000) //65K (This will guarantee a reasonable amount of allocations)
427                 toString();
428             else
429             {
430                 //128 is a reasonable start, this way, no really small operation should matter on performance
431                 strings.length = strings.length == 0 ? 128 : strings.length * 2;
432             }
433         }
434         strings[stringsPtr++] = value;
435     }
436     string toString()
437     {
438         import core.stdc.string:memcpy;
439         if(stringsPtr == 0) return cast(string)builtString[0..builtLength];
440         uint count = builtLength;
441         uint i = builtLength;
442         foreach(s;strings[0..stringsPtr])
443             count+= s.length;
444         builtString.length = count;
445         
446         foreach(s; strings[0..stringsPtr])
447         {
448             memcpy(builtString.ptr+i, s.ptr, s.length);
449             i+= s.length;
450         }
451         builtLength = count;
452         stringsPtr = 0;
453         return cast(string)builtString[0..builtLength];
454     }
455     auto ref opAssign(T)(T value) if(is(T == string))
456     {
457         builtString.length = value.length;
458         foreach(i, c; s)
459             builtString[i] = c;
460         stringsPtr = 0;
461         builtLength = cast(typeof(builtLength))value.length;
462 
463         return this;
464     }
465     auto ref opOpAssign(string op, T)(T value) if(op == "~")
466     {
467         import hip.util.reflection:isArray;
468         static if(isArray!T && !is(T == string))
469             foreach(v; value) append(v);
470         else
471             append(value);
472         return this;
473     }
474     ref auto opIndex(size_t index){return toString()[index];}
475     uint length(){return builtLength;}
476     ~this(){strings.length = 0;}
477 
478     ///Interface for OutputRange
479     alias put = append;
480 }
481 
482 
483 pure dstring toUTF32(string encoded)
484 {
485     dstring decoded;
486     version(UseDRuntimeDecoder)
487     {
488         foreach(dchar ch; encoded) decoded~= ch;
489     }
490     else
491     {
492         static import std.utf;
493         decoded = std.utf.toUTF32(encoded);
494     }
495     return decoded;
496 }
497 
498 pure string replaceAll(string str, char what, string replaceWith = "") @trusted nothrow
499 {
500     if(replaceWith.length == 1)
501     {
502         import hip.util.array;
503         char[] ret = uninitializedArray!(char[])(str.length);
504         foreach(i, ch; str)
505         {
506             if(ch == what)
507                 ret[i] = replaceWith[0];
508             else
509                 ret[i] = ch;
510         }
511         return cast(string)ret;
512     }
513     else
514     {
515         string ret;
516         for(int i = 0; i < str.length; i++)
517         {
518             if(str[i] != what) ret~= str[i];
519             else if(replaceWith != "") ret~=replaceWith;
520         }
521         return ret;
522     }
523 }
524 
525 pure string replaceAll(string str, string what, string replaceWith = "")
526 {
527     char[] ret;
528     int last;
529     int i;
530     do
531     {
532         i = indexOf(str, what, i);
533         if(i != -1)
534         {
535             int copyLength = i - last;
536             int currLength = cast(int)ret.length;
537             ret.length+= copyLength+replaceWith.length;
538             //Copy old content
539             ret[currLength..currLength+copyLength] = str[last..i];
540             //Copy replace
541             ret[currLength+copyLength..$] = replaceWith[];
542             //Skip what
543             i+= what.length;
544             last = i;
545         }
546     } while(i != -1);
547 
548     int copyLength = cast(int)(str.length - last);
549     int currLength = cast(int)ret.length;
550     ret.length+= copyLength;
551     ret[currLength..$] = str[last..$];
552 
553     return cast(string)ret;
554 }
555 
556 pure int indexOf(String str, const char[] toFind, int startIndex = 0) nothrow @nogc @safe
557 {
558     return indexOf(str.toString, toFind, startIndex);
559 }
560 
561 pure int indexOf(const char[] str, const char[] toFind, int startIndex = 0) nothrow @nogc @safe
562 {
563     if(!toFind.length)
564         return -1;
565     int left = 0;
566 
567     for(int i = startIndex; i < str.length; i++)
568     {
569         if(str[i] == toFind[left])
570         {
571             left++;
572             if(left == toFind.length)
573                 return (i+1) - left; //Remember that left is already out of bounds
574         }
575         else if(left > 0)
576             left--;
577     }
578     return -1;
579 }
580 
581 pure bool startsWith(inout string str, inout string withWhat) nothrow @nogc @safe
582 {
583     if(withWhat.length > str.length)
584         return false;
585     return str[0..withWhat.length] == withWhat;
586 }
587 
588 /**
589 *   Same thing as startsWith, but returns the part after the afterWhat
590 */
591 pure string after(string str, const string afterWhat) nothrow @nogc @safe
592 {
593     if(afterWhat.length > str.length || str[0..afterWhat.length] != afterWhat) return null;
594     return str[afterWhat.length..$];
595 }
596 
597 pure inout(string) findAfter(inout string str, inout string afterWhat, int startIndex = 0) nothrow @nogc @safe
598 {
599     int afterWhatIndex = str.indexOf(afterWhat, startIndex);
600     if(afterWhatIndex == -1)
601         return null;
602     return str[afterWhatIndex+afterWhat.length..$];
603 }
604 
605 /**
606 *   Returns the content that is between `left` and `right`:
607 ```d
608 string test = `string containing a "thing"`;
609 writeln(test.between(`"`, `"`)); //thing
610 ```
611 */
612 pure inout(string) between(inout string str, inout string left, inout string right, int start = 0) nothrow @nogc @safe
613 {
614     int leftIndex = str.indexOf(left, start);
615     if(leftIndex == -1) return null;
616     int rightIndex = str.indexOf(right, leftIndex+1);
617     if(rightIndex == -1) return null;
618 
619     return str[leftIndex+1..rightIndex];
620 }
621 
622 pure int indexOf(const string str, char ch, int startIndex = 0) nothrow @nogc @trusted
623 {
624     for(; startIndex < str.length; startIndex++)
625         if(str[startIndex] == ch)
626             return startIndex;
627     return -1;
628 }
629 
630 
631 string repeat(string str, size_t repeatQuant)
632 {
633     string ret;
634     for(int i = 0; i < repeatQuant; i++)
635         ret~= str;
636     return ret;
637 }
638 
639 pure int count(const string str, const string countWhat) nothrow @nogc @safe
640 {
641     int ret = 0;
642     int index = 0;
643 
644     //Navigates using indexOf
645     while((index = str.indexOf(countWhat, index)) != -1)
646     {
647         index+= countWhat.length;
648         ret++;
649     }
650     return ret;
651 }
652 
653 alias countUntil = indexOf;
654 
655 int lastIndexOf(const string str, const string toFind, int startIndex = -1) pure nothrow @nogc @safe
656 {
657     if(startIndex == -1) startIndex = cast(int)(str.length)-1;
658 
659     int maxToFind = cast(int)toFind.length - 1;
660     int right = maxToFind;
661     if(right < 0) return -1; //Empty string case 
662     
663     
664     for(int i = startIndex; i >= 0; i--)
665     {
666         if(str[i] == toFind[right])
667         {
668             right--;
669             if(right == -1)
670                 return i;
671         }
672         else if(right < maxToFind)
673             right++;
674     }
675     return -1;
676 }
677 int lastIndexOf(string str, char ch, int startIndex = -1) pure nothrow @nogc @trusted
678 {
679     return lastIndexOf(str, cast(string)(&ch)[0..1], startIndex);
680 }
681 
682 T toDefault(T)(string s, T defaultValue = T.init)
683 {
684     if(s == "")
685         return defaultValue;
686     T v = defaultValue;
687     try{v = to!(T)(s);}
688     catch(Exception e){}
689     return v;
690 }
691 
692 string fromStringz(const char* cstr) pure nothrow @nogc
693 {
694     import core.stdc.string:strlen;
695     size_t len = strlen(cstr);
696     return (len) ? cast(string)cstr[0..len] : null;
697 }
698 
699 const(char)* toStringz(string str) pure nothrow
700 {
701     return (str~"\0").ptr;
702 }
703 pragma(inline, true) char toLowerCase(char c) pure nothrow @safe @nogc 
704 {
705     return (c < 'A' || c > 'Z') ? c : cast(char)(c + ('a' - 'A'));
706 }
707 
708 string toLowerCase(string str)
709 {
710     char[] ret = new char[](str.length);
711     for(uint i = 0; i < str.length; i++)
712         ret[i] = str[i].toLowerCase;
713     return cast(string)ret;
714 }
715 
716 pragma(inline, true) char toUpper(char c) pure nothrow @nogc @safe
717 {
718     if(c < 'a' || c > 'z')
719         return c;
720     return cast(char)(c - ('a' - 'A'));
721 }
722 
723 string toUpper(string str) pure nothrow @safe
724 {
725     char[] ret = new char[](str.length);
726     for(uint i = 0; i < str.length; i++)
727         ret[i] = str[i].toUpper;
728     return ret;
729 }
730 
731 string[] split(string str, char separator) pure nothrow
732 {
733     return split(str, cast(string)(&separator)[0..1]);
734 }
735 
736 string[] split(string str, string separator) pure nothrow @safe
737 {
738     string[] ret;
739     int last = 0;
740     int index = 0;
741     do
742     {
743         index = str.indexOf(separator, index);
744         if(index != -1)
745         {
746             ret~= str[last..index];
747         	last = index+= separator.length;
748         }
749     }
750     while(index != -1);
751     if(last != index)
752         ret~= str[last..$];
753     return ret;
754 }
755 
756 auto splitRange(TString, TStrSep)(TString str, TStrSep separator) pure nothrow @safe @nogc
757 {
758     struct SplitRange
759     {
760         TString strToSplit;
761         TStrSep sep;
762         TString frontStr;
763         int lastFound, index;
764 
765         bool empty(){return frontStr == null && index == -1 && lastFound == -1;}
766         TString front()
767         {
768             if(frontStr == "") popFront();
769             return frontStr;
770         }
771         void popFront()
772         {
773             if(index == -1 && lastFound == -1)
774             {
775                 frontStr = null;
776                 return;
777             }
778             index = indexOf(cast(TString)strToSplit, cast(TStrSep)sep, index);
779             //When finding, take the string[lastFound..index]
780             if(index != -1)
781             {
782                 frontStr = strToSplit[lastFound..index];
783                 lastFound = index+= sep.length;
784             }
785             //If index not found and there was a last, take the string[lastFound..$]
786             else if(lastFound != 0)
787             {
788                 frontStr = strToSplit[lastFound..$];
789                 lastFound = -1;
790             }
791             //Just say there is no string
792             else
793                 lastFound = -1;
794         }
795     }
796 
797     return SplitRange(str, separator);
798 }
799 
800 
801 bool isNumber(const string str) nothrow @nogc
802 {
803     if(!str)
804         return false;
805     bool isFirst = true;
806     bool hasDecimalSeparator = false;
807     for(int i = 0; i < str.length; i++)
808     {
809         char c = str[i];
810         //Check for negative
811         if(isFirst)
812         {
813             isFirst = false;
814             if(c == '-')
815                 continue;
816         }
817         //Can only check for '.' once.
818         if(!hasDecimalSeparator && c == '.')
819             hasDecimalSeparator = true;
820         else if(c < '0' || c > '9')
821             return false;
822 
823     }
824     return true;
825 }
826 
827 pragma(inline, true)
828 string toString(string s) nothrow @nogc pure @safe { return s; }
829 
830 /**
831  * Returns the entire string if input is not a number separated by a '.'
832  *
833  *
834  * Params:
835  *   input = A input string in "523.987"
836  *   decimalPlaces = How many decimal places it must contain.
837  * Returns: A slice which removes places after the decimal case if there exists more than it should
838  */
839 string limitDecimalPlaces(string input, ubyte decimalPlaces) @nogc nothrow
840 {
841     string str = input.toString;
842     if(!isNumber(str))
843         return input;
844     ptrdiff_t decIndex = indexOf(str, ".");
845     if(decIndex == -1)
846         return input;
847 
848     //+1 since there is also the dot
849     size_t end = decIndex+1+decimalPlaces;
850     if(end > input.length)
851         end = input.length;
852     return input[0..end];
853 
854 }
855 
856 /**
857 This function will get the number at the end of the string. Used when you have numbered items such as frames:
858 walk_01, walk_02, etc
859 ```d
860 "test123".getNumericEnding == "123"
861 "123abc".getNumericEnding == ""
862 "123".getNumericEnding == "123"
863 ```
864 */
865 string getNumericEnding(string s)
866 {
867     if(!s)
868         return "";
869     ptrdiff_t i = cast(ptrdiff_t)s.length - 1;
870     while(i >= 0)
871     {
872         if(!isNumeric(s[i]))
873             return s[i+1..$];
874         i--;
875     }
876     return s;
877 }
878 
879 
880 pragma(inline, true) bool isUpperCase(TChar)(TChar c) @nogc nothrow pure @safe
881 {
882     return c >= 'A' && c <= 'Z';
883 }
884 pragma(inline, true) bool isLowercase(TChar)(TChar c) @nogc nothrow pure @safe
885 {
886     return c >= 'a' && c <= 'z';
887 }
888 
889 pragma(inline, true) bool isAlpha(TChar)(TChar c) @nogc nothrow pure @safe
890 {
891     return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
892 }
893 
894 pragma(inline, true) bool isEndOfLine(TChar)(TChar c) @nogc nothrow pure @safe
895 {
896     return c == '\n' || c == '\r';
897 }
898 
899 pragma(inline, true) bool isNumeric(TChar)(TChar c) @nogc nothrow pure @safe
900 {
901     return (c >= '0' && c <= '9') || (c == '-');
902 }
903 pragma(inline, true) bool isWhitespace(TChar)(TChar c) @nogc nothrow pure @safe
904 {
905     return (c == ' ' || c == '\t' || c.isEndOfLine);
906 }
907 
908 string trim(string str) pure nothrow @safe @nogc
909 {
910     size_t start = 0;
911     size_t end = str.length;
912     while(start < end && str[start].isWhitespace)
913         start++;
914     while(end > start && str[end-1].isWhitespace)
915         end--;
916     return str[start..end];
917 }
918 
919 string join(string[] args, string separator = "")
920 {
921 	string ret = args.length > 0 ? args[0] : null;
922 	for(int i = 1; i < args.length; i++)
923 		ret~= separator~args[i];
924 	return ret;
925 }
926 
927 unittest
928 {
929     assert(join(["hello", "world"], ", ") == "hello, world");
930     assert(split("hello world", " ").length == 2);
931     assert(toDefault!int("hello") == 0);
932     assert(lastIndexOf("hello, hello", "hello") == 7);
933     assert(indexOf("hello, hello", "hello") == 0);
934     assert(replaceAll("\nTest\n", '\n') == "Test");
935 
936     assert(trim(" \n  \thello there  \n \t") == "hello there");
937     assert(between(`string containing a "thing"`, `"`, `"`) == "thing");
938 
939     assert("test123".getNumericEnding == "123");
940     assert("123abc".getNumericEnding == "");
941     assert("123".getNumericEnding == "123");
942 }